using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using Charisma.Common;

namespace Charisma
{
    public static class ReflectionService
    {
        private static readonly ConcurrentDictionary<Type, DynamicTypeWrapper> dynamicWrapperCache =
            new ConcurrentDictionary<Type, DynamicTypeWrapper>(2, 4);

        public static Func<object[], object> BuildFactoryMethod(Type type, ConstructorInfo ctorInfo)
        {
            Contract.Requires(type != null);
            Contract.Requires(type.IsArray ? ctorInfo == null : ctorInfo != null);
            Contract.Requires(!type.IsInterface && !type.IsGenericTypeDefinition && !type.IsGenericParameter);

            Module module = type.Module;
            DynamicMethod ctorMethod = type.IsArray
                                           ? new DynamicMethod(".ctor",
                                                               MethodAttributes.Public | MethodAttributes.Static,
                                                               CallingConventions.Standard, typeof(object),
                                                               new[] { typeof(object[]) }, module, true)
                                           : new DynamicMethod(".ctor",
                                                               MethodAttributes.Public | MethodAttributes.Static,
                                                               CallingConventions.Standard, typeof(object),
                                                               new[] { typeof(object[]) }, type, true);
            ILGenerator il = ctorMethod.GetILGenerator(64);
            Contract.Assume(il != null);
            if (type.IsValueType) {
                il.DeclareLocal(type);
                il.Emit(OpCodes.Ldloca_S, 0);
                il.Emit(OpCodes.Initobj, type);
                il.Emit(OpCodes.Ldloc_0);
                il.Emit(OpCodes.Box, type);
                il.Emit(OpCodes.Ret);
            }
            else if (type.IsArray) {
                Type listType = typeof(List<>).MakeGenericType(type.GetElementType());
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Castclass, listType);
                il.Emit(OpCodes.Call, listType.GetMethod("ToArray"));
                il.Emit(OpCodes.Ret);
            }
            else {
                ParameterInfo[] parameters = ctorInfo.GetParameters();
                if (parameters.Length != 0) {
                    int byRefParam = 0;
                    for (int index = 0; index < parameters.Length; index++) {
                        ParameterInfo parameter = parameters[index];
                        Type paramType = parameter.ParameterType;
                        if (paramType.IsByRef)
                            il.Emit(OpCodes.Ldloca_S, byRefParam++);
                        else {
                            il.Emit(OpCodes.Ldarg_0);
                            il.Emit(OpCodes.Ldc_I4, index);
                            il.Emit(OpCodes.Ldelem_Ref);
                            if (paramType != typeof(object))
                                il.Emit(paramType.IsValueType
                                            ? OpCodes.Unbox_Any
                                            : OpCodes.Castclass, paramType);
                        }
                    }
                }
                il.Emit(OpCodes.Newobj, ctorInfo);
                il.Emit(OpCodes.Ret);
            }
            return (Func<object[], object>)ctorMethod.CreateDelegate(typeof(Func<object[], object>));
        }

        public static T CreateInstance<T>(Type type, params object[] args) where T : class
        {
            Contract.Requires(type != null);
            Contract.Requires(args != null);
            Contract.Ensures(Contract.Result<T>() != null);

            return (T)Activator.CreateInstance(type, args);
        }

        public static Func<object, object> MakeGetter(Type ownerType, MemberInfo memberInfo)
        {
            Contract.Requires(ownerType != null);
            Contract.Requires(memberInfo != null);
            Contract.Requires(memberInfo is PropertyInfo || memberInfo is FieldInfo);
            Contract.Requires(memberInfo is FieldInfo || ((PropertyInfo)memberInfo).CanRead);
            Contract.Ensures(Contract.Result<Func<object, object>>() != null);

            var getter = new DynamicMethod("getter",
                                           MethodAttributes.Public | MethodAttributes.Static,
                                           CallingConventions.Standard, typeof(object),
                                           new[] { typeof(object) }, ownerType, true);
            ILGenerator il = getter.GetILGenerator(32);
            Contract.Assume(il != null);
            if (ownerType.IsValueType) {
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Unbox, ownerType);
            }
            else
                il.Emit(OpCodes.Ldarg_0);

            var propertyInfo = memberInfo as PropertyInfo;
            Type type;
            if (propertyInfo != null) {
                type = propertyInfo.PropertyType;
                il.Emit(OpCodes.Call, propertyInfo.GetGetMethod(true));
            }
            else {
                var fieldInfo = (FieldInfo)memberInfo;
                type = fieldInfo.FieldType;
                il.Emit(OpCodes.Ldfld, fieldInfo);
            }
            if (type.IsValueType)
                il.Emit(OpCodes.Box, type);
            il.Emit(OpCodes.Ret);

            return (Func<object, object>)getter.CreateDelegate(typeof(Func<object, object>));
        }

        public static Action<object, object> MakeSetter(Type ownerType, MemberInfo memberInfo)
        {
            Contract.Requires(ownerType != null);
            Contract.Requires(memberInfo != null);
            Contract.Requires(memberInfo is PropertyInfo || memberInfo is FieldInfo);
            Contract.Requires(memberInfo is FieldInfo || ((PropertyInfo)memberInfo).CanWrite);
            Contract.Requires(memberInfo is PropertyInfo || !((FieldInfo)memberInfo).IsInitOnly);
            Contract.Ensures(Contract.Result<Action<object, object>>() != null);

            var getter = new DynamicMethod("setter",
                                           MethodAttributes.Public | MethodAttributes.Static,
                                           CallingConventions.Standard, typeof(void),
                                           new[] { typeof(object), typeof(object) }, ownerType, true);
            ILGenerator il = getter.GetILGenerator(32);
            Contract.Assume(il != null);
            if (ownerType.IsValueType) {
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Unbox, ownerType);
            }
            else
                il.Emit(OpCodes.Ldarg_0);

            il.Emit(OpCodes.Ldarg_1);
            var propertyInfo = memberInfo as PropertyInfo;
            if (propertyInfo != null) {
                Type type = propertyInfo.PropertyType;
                if (type.IsValueType)
                    il.Emit(OpCodes.Unbox_Any, type);
                il.Emit(OpCodes.Call, propertyInfo.GetSetMethod(true));
            }
            else {
                var fieldInfo = (FieldInfo)memberInfo;
                Type type = fieldInfo.FieldType;
                if (type.IsValueType)
                    il.Emit(OpCodes.Unbox_Any, type);
                il.Emit(OpCodes.Stfld, fieldInfo);
            }
            il.Emit(OpCodes.Ret);

            return (Action<object, object>)getter.CreateDelegate(typeof(Action<object, object>));
        }

        /// <summary>
        /// Converts provided <paramref name="type"/> instance to the dynamic wrapper allowing to call static members dynamically.
        /// </summary>
        /// <param name="type">The type.</param>
        public static dynamic ToDynamic(this Type type)
        {
            return dynamicWrapperCache.GetOrAdd(type, t => new DynamicTypeWrapper(t));
        }

        [Pure]
        public static Type MakeGenericType(Type definition, Type parameter)
        {
            Contract.Requires(definition != null);
            Contract.Requires(parameter != null);
            Contract.Requires(definition.IsGenericTypeDefinition);
            Contract.Requires(definition.GetGenericArguments().Length == 1);

            return definition.MakeGenericType(parameter);
        }

        public static MethodInfo GetGenericMethod(Type type, string methodName, params Type[] arguments)
        {
            Contract.Requires(type != null);
            Contract.Requires(!String.IsNullOrEmpty(methodName));
            Contract.Requires(arguments != null);

            MethodInfo methodInfo = type.GetMethod(methodName,
                                                   BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic |
                                                   BindingFlags.Static);
            if (methodInfo == null || !methodInfo.IsGenericMethodDefinition)
                throw new InvalidOperationException();

            return methodInfo.MakeGenericMethod(arguments);
        }

        /// <summary>
        /// Gets the generic type of <see cref="IEnumerable{T}"/> where <paramref name="elementType"/> is generic argument.
        /// </summary>
        /// <param name="elementType">Type of the element.</param>
        [Pure]
        public static Type GetSequenceType(this Type elementType)
        {
            Contract.Requires(elementType != null);

            return typeof(IEnumerable<>).MakeGenericType(elementType);
        }

        /// <summary>
        /// Gets element type of the sequence.
        /// </summary>
        /// <param name="sequenceType">Type of the sequence.</param>
        [Pure]
        public static Type GetElementType(Type sequenceType)
        {
            Contract.Requires(sequenceType != null);

            Type ienum = FindIEnumerable(sequenceType);
            return ienum == null
                       ? typeof(object)
                       : ienum.GetGenericArguments()[0];
        }

        private static Type FindIEnumerable(this Type sequenceType)
        {
            Contract.Requires(sequenceType != null);

            if (sequenceType == null || sequenceType == typeof(string))
                return null;
            if (sequenceType.IsArray)
                return typeof(IEnumerable<>).MakeGenericType(sequenceType.GetElementType());
            if (sequenceType.IsGenericType)
                foreach (Type arg in sequenceType.GetGenericArguments()) {
                    Type enumerable = typeof(IEnumerable<>).MakeGenericType(arg);
                    if (enumerable.IsAssignableFrom(sequenceType))
                        return enumerable;
                }
            Type[] interfaces = sequenceType.GetInterfaces();
            if (interfaces.Length > 0)
                foreach (Type enumerable in interfaces.Select(FindIEnumerable).Where(t => t != null)) {
                    return enumerable;
                }
            if (sequenceType.BaseType != null && sequenceType.BaseType != typeof(object))
                return FindIEnumerable(sequenceType.BaseType);
            return null;
        }

        /// <summary>
        /// Determines whether <paramref name="type"/> implements the <paramref name="interface"/>.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <param name="interface">The <see langword="interface"/>.</param>
        /// <returns>
        ///  <see langword="true"/> if the specified <paramref name="type"/> implements the <paramref name="interface"/>;
        /// otherwise, <see langword="false"/>.
        /// </returns>
        [Pure]
        public static bool IsOfGenericInterface(this Type type, Type @interface)
        {
            Contract.Requires(type != null);
            Contract.Requires(@interface != null);

            return type.IsOfGenericType(@interface) || type.GetInterfaces().Any(t => t.IsOfGenericType(@interface));
        }


        /// <summary>
        /// Determines whether the specified <paramref name="type"/> inherits the generic <paramref name="baseType"/>.
        /// </summary>
        /// <param name="type">The type to check.</param>
        /// <param name="baseType">Type of the generic.</param>
        /// <returns>
        /// <see langword="true"/> if the specified <paramref name="type"/> inherits the generic <paramref name="baseType"/>;
        /// otherwise, <see langword="false"/>.
        /// </returns>
        [Pure]
        public static bool IsOfGenericType(this Type type, Type baseType)
        {
            return GetGenericType(type, baseType) != null;
        }

        /// <summary>
        /// Determines whether the specified <paramref name="type"/> inherits 
        /// the generic <paramref name="baseType"/> and returns direct inheritor of generic <paramref name="baseType"/> if any.
        /// </summary>
        /// <param name="type">The type to check.</param>
        /// <param name="baseType">Type of the generic.</param>
        /// <returns>
        /// Generic <see cref="Type"/> that directly inherits <paramref name="baseType"/> if the specified <paramref name="type"/> inherits the generic <paramref name="baseType"/>;
        /// otherwise, <see langword="null"/>.
        /// </returns>
        [Pure]
        public static Type GetGenericType(this Type type, Type baseType)
        {
            Type t = type;
            while (!(t == null || t == typeof(object))) {
                if (t.IsGenericType && t.GetGenericTypeDefinition() == baseType)
                    return t;
                t = t.BaseType;
            }
            return null;
        }

        [Pure]
        public static bool Is<T>(this Type value)
        {
            Contract.Requires(value != null);
            return typeof(T).IsAssignableFrom(value);
        }

        /// <summary>
        /// Indicates whether <paramref name="type"/> is a <see cref="Nullable{T}"/> type.
        /// </summary>
        /// <param name="type">Type to check.</param>
        /// <returns><see langword="True"/> if type is nullable type;
        /// otherwise, <see langword="false"/>.</returns>
        [Pure]
        public static bool IsNullable(this Type type)
        {
            Contract.Requires(type != null);

            return type.IsValueType && type.IsGenericType &&
                   type.GetGenericTypeDefinition() == typeof(Nullable<>);
        }

        /// <summary>
        /// Indicates whether <typeparamref name="T"/> type is a <see cref="Nullable{T}"/> type.
        /// </summary>
        /// <typeparam name="T">Type to check.</typeparam>
        /// <returns><see langword="True"/> if type is nullable type;
        /// otherwise, <see langword="false"/>.</returns>
        [Pure]
        public static bool IsNullable<T>()
        {
            return ReferenceEquals(default(T), null) && typeof(T).IsValueType;
        }

        /// <summary>
        /// Converts <paramref name="type"/> to type that can assign both values of <paramref name="type"/> and <see landword="null"/>.
        /// This method is a reverse for <see cref="StripNullable"/> method.
        /// </summary>
        /// <param name="type">A type to convert.</param>
        /// <returns>
        /// If <paramref name="type"/> is a reference type or a <see cref="Nullable{T}"/> instance returns <paramref name="type"/>.
        /// Otherwise returns <see cref="Nullable{T}"/> of <paramref name="type"/>. 
        /// </returns>
        [Pure]
        public static Type ToNullable(this Type type)
        {
            Contract.Requires(type != null);

            return type.IsValueType && !type.IsNullable()
                       ? typeof(Nullable<>).MakeGenericType(type)
                       : type;
        }

        /// <summary>
        /// Converts <paramref name="type"/> to <see cref="Nullable{T}"/> if <paramref name="type"/> is a value type.
        /// Otherwise returns just <paramref name="type"/>.
        /// This method is a reverse for <see cref="ToNullable"/> method.
        /// </summary>
        /// <param name="type">The type to process.</param>
        /// <returns>
        /// <see cref="Nullable{T}"/> of <paramref name="type"/> is specified type is a value type.
        /// Otherwise return just <paramref name="type"/>.
        /// </returns>
        [Pure]
        public static Type StripNullable(this Type type)
        {
            Contract.Requires(type != null);
            Contract.Requires(!type.IsGenericType || type.GetGenericArguments().Length > 0);

            return type.IsNullable()
                       ? type.GetGenericArguments()[0]
                       : type;
        }

        /// <summary>
        /// Determines whether the specified <paramref name="type"/> is anonymous type.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <returns>
        ///   <see langword="true" /> if the specified type is anonymous; otherwise, <see langword="false" />.
        /// </returns>
        [Pure]
        public static bool IsAnonymous(this Type type)
        {
            Contract.Requires(type != null);

            return ((type.Name.StartsWith("<>") || type.Name.StartsWith("VB$"))
                    && type.BaseType == typeof(object)
                    && Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute), false)
                    && type.Name.Contains("AnonymousType")
                    && (type.Attributes & TypeAttributes.NotPublic) == TypeAttributes.NotPublic);
        }

        /// <summary>
        /// Determines whether the specified <paramref name="type"/> is closure type.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <returns>
        ///   <see langword="true" /> if the specified type is anonymous; otherwise, <see langword="false" />.
        /// </returns>
        [Pure]
        public static bool IsClosure(this Type type)
        {
            Contract.Requires(type != null);

            return ((type.Name.StartsWith("<>") || type.Name.StartsWith("VB$"))
                    && type.BaseType == typeof(object)
                    && Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute), false)
                    && type.Name.Contains("DisplayClass")
                    && (type.Attributes & TypeAttributes.NotPublic) == TypeAttributes.NotPublic);
        }

        public static Type GetGenericImplementorType(Type type)
        {
            Contract.Requires(type != null);

            Type result;

            if (type.IsInterface && type.IsGenericType
                && type.GetGenericTypeDefinition() == Wellknown.Types.GenericRepository)
                result = type;
            else
                result = type.GetInterfaces()
                    .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == Wellknown.Types.GenericRepository)
                    .FirstOrDefault();

            return result;
        }

        public static MemberInfo GetMember<T, TValue>(Expression<Func<T, TValue>> expression)
        {
            Contract.Requires(expression != null);

            var me = expression.Body as MemberExpression;
            if (me == null)
                throw new NotSupportedException(
                    string.Format("Expression other then {0} are not supported yet", typeof(MemberExpression).Name));

            return me.Member;
        }

        public static Type GetMemberType(this MemberInfo field)
        {
            Contract.Requires(field != null);
            Contract.Requires(field is PropertyInfo || field is FieldInfo);
            Contract.Ensures(Contract.Result<Type>() != null);

            var propertyInfo = field as PropertyInfo;
            return propertyInfo != null
                       ? propertyInfo.PropertyType
                       : ((FieldInfo)field).FieldType;
        }

        /// <summary>
        /// Finds all dependent assemblies in current <see cref="AppDomain"/>.
        /// </summary>
        /// <param name="target">The assembly.</param>
        /// <returns>A <see cref="IList{T}"/> of all dependent assemblies.</returns>
        public static IEnumerable<Assembly> GetReferencing(this Assembly target)
        {
            string name = target.GetName().Name;
            var candidates = AppDomain.CurrentDomain.GetAssemblies();

            return candidates
                .Where(c => c.GetReferencedAssemblies().Any(a => a.Name == name));
        }

    }
}